/**
* Revelor Search Bundle
* Combined and bundled JavaScript for Shoptet
*
* Contains:
* - Console Wrapper (admin-only logging)
* - DataLayer Helper (GTM analytics)
* - Search Tracker (backend tracking)
* - Search Overlay (main search functionality)
* - Search Results Page (results page logic)
*
* Build date: 2025-11-01T00:12:21.753Z
* Project: Revelor Textilomanie
*/
// =============================================================================
// console-wrapper.js
// =============================================================================
/**
* @fileoverview Console Wrapper for Admin-Only Logging
* Přepisuje console metody - výstupy se zobrazí pouze pokud je admin přihlášený
* @version 1.0.0
* @author Revelor by Webotvůrci
*/
(function() {
'use strict';
/**
* Zkontroluje, jestli je admin přihlášený
* Kontroluje dynamicky při každém volání (BODY může být ještě ne načtený)
* @returns {boolean}
*/
function isAdminLoggedIn() {
// Pokud BODY ještě není načtené, vrať false (bezpečnější)
if (!document.body) {
return false;
}
// Zkontroluj, jestli má BODY class 'admin-logged'
return document.body.classList.contains('admin-logged');
}
/**
* Uložit původní console metody
*/
const originalConsole = {
log: console.log.bind(console),
error: console.error.bind(console),
warn: console.warn.bind(console),
info: console.info.bind(console),
debug: console.debug.bind(console),
trace: console.trace.bind(console),
table: console.table.bind(console),
group: console.group.bind(console),
groupEnd: console.groupEnd.bind(console),
groupCollapsed: console.groupCollapsed.bind(console),
time: console.time.bind(console),
timeEnd: console.timeEnd.bind(console),
assert: console.assert.bind(console)
};
/**
* No-op funkce (nic nedělá)
*/
function noop() {}
/**
* Wrapper funkce, která zkontroluje admin status
*/
function createConsoleWrapper(originalMethod) {
return function(...args) {
if (isAdminLoggedIn()) {
originalMethod.apply(console, args);
}
// Pokud není admin, nic se nestane (no-op)
};
}
// Přepiš console metody pouze pokud ještě nejsou přepsané
if (!window._revelorConsoleWrapped) {
console.log = createConsoleWrapper(originalConsole.log);
console.error = createConsoleWrapper(originalConsole.error);
console.warn = createConsoleWrapper(originalConsole.warn);
console.info = createConsoleWrapper(originalConsole.info);
console.debug = createConsoleWrapper(originalConsole.debug);
console.trace = createConsoleWrapper(originalConsole.trace);
console.table = createConsoleWrapper(originalConsole.table);
console.group = createConsoleWrapper(originalConsole.group);
console.groupEnd = createConsoleWrapper(originalConsole.groupEnd);
console.groupCollapsed = createConsoleWrapper(originalConsole.groupCollapsed);
console.time = createConsoleWrapper(originalConsole.time);
console.timeEnd = createConsoleWrapper(originalConsole.timeEnd);
console.assert = createConsoleWrapper(originalConsole.assert);
// Označit, že jsme už wrapper aplikovali
window._revelorConsoleWrapped = true;
}
})();
// =============================================================================
// datalayer-helper.js
// =============================================================================
/**
* @fileoverview DataLayer Helper for Revelor Search Analytics
* Manages all dataLayer pushes for Google Tag Manager
* @version 2.0.0
* @author Revelor by Webotvůrci
* @contact [email protected] | +420 728 089 029
*/
class RevelorDataLayer {
constructor() {
// Initialize dataLayer if not exists
window.dataLayer = window.dataLayer || [];
// Session storage for tracking search journey
this.sessionKey = 'revelor_search_session';
this.initSession();
}
/**
* Initialize search session
*/
initSession() {
if (!sessionStorage.getItem(this.sessionKey)) {
sessionStorage.setItem(this.sessionKey, JSON.stringify({
searchCount: 0,
lastSearchTerm: null,
lastSearchTime: null,
searchStartTime: null
}));
}
}
/**
* Get search session data
*/
getSession() {
return JSON.parse(sessionStorage.getItem(this.sessionKey) || '{}');
}
/**
* Update search session
*/
updateSession(updates) {
const session = this.getSession();
const newSession = { ...session, ...updates };
sessionStorage.setItem(this.sessionKey, JSON.stringify(newSession));
}
/**
* Get current timestamp in ISO format
*/
getTimestamp() {
return new Date().toISOString();
}
/**
* Get user type (new vs returning)
*/
getUserType() {
// Check if user has visited before (using cookie or localStorage)
return localStorage.getItem('user_visited') ? 'returning' : 'new';
}
/**
* Get page type from URL or body class
*/
getPageType() {
const path = window.location.pathname;
if (path === '/' || path === '') return 'homepage';
if (path.includes('/produkt/') || path.includes('/product/')) return 'product';
if (path.includes('/kategorie/') || path.includes('/category/')) return 'category';
if (path.includes('/vysledky-hledani') || path.includes('/search')) return 'search_results';
return 'other';
}
/**
* Calculate time difference in seconds
*/
getTimeDiff(startTime) {
if (!startTime) return 0;
return ((Date.now() - new Date(startTime).getTime()) / 1000).toFixed(1);
}
/**
* Push event to dataLayer
*/
push(eventData) {
console.log('📊 Revelor DataLayer Push:', eventData);
window.dataLayer.push(eventData);
}
/**
* 1. Search Started
*/
searchStarted(context = 'overlay') {
const session = this.getSession();
this.push({
event: 'revelor_search_started',
search_context: context,
timestamp: this.getTimestamp(),
user_type: this.getUserType(),
page_type: this.getPageType(),
session_search_count: session.searchCount || 0
});
// Update session
this.updateSession({
searchStartTime: this.getTimestamp(),
searchCount: (session.searchCount || 0) + 1
});
}
/**
* 2. Search Query Submitted
*/
searchQuerySubmitted(searchTerm, context = 'overlay') {
const session = this.getSession();
const isRefinement = session.lastSearchTerm && session.lastSearchTerm !== searchTerm;
this.push({
event: 'revelor_search_query_submitted',
search_term: searchTerm,
search_term_length: searchTerm.length,
search_context: context,
is_refinement: isRefinement,
previous_search_term: isRefinement ? session.lastSearchTerm : null,
timestamp: this.getTimestamp()
});
// Update session
this.updateSession({
lastSearchTerm: searchTerm,
lastSearchTime: this.getTimestamp()
});
}
/**
* 3. Search Results Displayed
*/
searchResultsDisplayed(searchTerm, resultsData, responseTime, page = 1) {
const totalResults = resultsData.total_products || 0;
const productsCount = resultsData.products?.length || 0;
const categoriesCount = resultsData.categories?.length || 0;
const articlesCount = resultsData.articles?.length || 0;
const brandsCount = resultsData.brands?.length || 0;
this.push({
event: 'revelor_search_results_displayed',
search_term: searchTerm,
results_count: {
total: totalResults,
products: productsCount,
categories: categoriesCount,
articles: articlesCount,
brands: brandsCount
},
has_results: totalResults > 0,
page: page,
response_time_ms: responseTime,
timestamp: this.getTimestamp()
});
}
/**
* 4. Search No Results
*/
searchNoResults(searchTerm) {
// Simple typo detection (contains numbers mixed with letters, etc.)
const potentialTypo = /\d/.test(searchTerm) || searchTerm.length < 3;
this.push({
event: 'revelor_search_no_results',
search_term: searchTerm,
search_term_length: searchTerm.length,
potential_typo: potentialTypo,
timestamp: this.getTimestamp()
});
}
/**
* 5. Search Result Click
*/
searchResultClick(searchTerm, resultType, resultData, position, totalResults) {
const session = this.getSession();
const clickTimeFromSearch = this.getTimeDiff(session.lastSearchTime);
const eventData = {
event: 'revelor_search_result_click',
search_term: searchTerm,
result_type: resultType,
result_position: position,
total_results: totalResults,
click_time_from_search: parseFloat(clickTimeFromSearch),
timestamp: this.getTimestamp()
};
// Add result-specific data
if (resultType === 'product') {
eventData.result_data = {
item_id: resultData._id || resultData.id,
item_name: resultData.title,
item_category: resultData.category || 'Neznámá kategorie',
price: resultData.price_s_dph || 0,
currency: 'CZK',
availability: resultData.skladem ? 'in_stock' : 'out_of_stock',
item_url: resultData.url
};
// GA4 Enhanced Ecommerce format
eventData.ecommerce = {
currency: 'CZK',
value: resultData.price_s_dph || 0,
items: [{
item_id: resultData._id || resultData.id,
item_name: resultData.title,
item_list_name: 'Search Results',
item_list_id: 'search_results',
price: resultData.price_s_dph || 0,
index: position,
quantity: 1
}]
};
} else if (resultType === 'category') {
eventData.result_data = {
category_name: resultData.title,
category_url: resultData.url
};
} else if (resultType === 'article') {
eventData.result_data = {
article_title: resultData.title,
article_url: resultData.url
};
}
this.push(eventData);
}
/**
* 6. Search Load More
*/
searchLoadMore(searchTerm, currentPage, totalPages, productsLoaded, totalProductsShown) {
this.push({
event: 'revelor_search_load_more',
search_term: searchTerm,
current_page: currentPage,
next_page: currentPage + 1,
total_pages: totalPages,
products_loaded: productsLoaded,
total_products_shown: totalProductsShown,
timestamp: this.getTimestamp()
});
}
/**
* 7. Search Refinement
*/
searchRefinement(originalTerm, refinedTerm, originalResults, refinedResults) {
// Determine refinement type
let refinementType = 'modification';
if (refinedTerm.length > originalTerm.length) {
refinementType = 'expansion';
} else if (refinedTerm.length < originalTerm.length) {
refinementType = 'narrowing';
} else if (Math.abs(refinedTerm.length - originalTerm.length) <= 2) {
refinementType = 'correction';
}
this.push({
event: 'revelor_search_refinement',
original_search_term: originalTerm,
refined_search_term: refinedTerm,
refinement_type: refinementType,
original_results_count: originalResults,
refined_results_count: refinedResults,
timestamp: this.getTimestamp()
});
}
/**
* 8. Search Suggestion Click
*/
searchSuggestionClick(typedQuery, suggestedTerm, position, suggestionType) {
this.push({
event: 'revelor_search_suggestion_click',
typed_query: typedQuery,
suggested_term: suggestedTerm,
suggestion_position: position,
suggestion_type: suggestionType,
timestamp: this.getTimestamp()
});
}
/**
* 9. Search Recommendations Shown
*/
searchRecommendationsShown(recommendationsData) {
const products = recommendationsData.products || [];
const categories = recommendationsData.categories || [];
this.push({
event: 'revelor_search_recommendations_shown',
recommendations_count: products.length + categories.length,
products_count: products.length,
categories_count: categories.length,
recommendation_type: 'default', // trending/popular/personalized
timestamp: this.getTimestamp()
});
}
/**
* 10. Search Category Expand
*/
searchCategoryExpand(searchTerm, expandType, action, visibleBefore, visibleAfter) {
this.push({
event: 'revelor_search_category_expand',
search_term: searchTerm,
expand_type: expandType, // 'categories' | 'articles'
action: action, // 'expand' | 'collapse'
visible_items_before: visibleBefore,
visible_items_after: visibleAfter,
timestamp: this.getTimestamp()
});
}
/**
* 11. Search Abandoned
*/
searchAbandoned(searchTerm, resultsCount, timeSpent, reason = 'closed_overlay') {
this.push({
event: 'revelor_search_abandoned',
search_term: searchTerm,
results_count: resultsCount,
time_spent: parseFloat(timeSpent),
reason: reason, // 'closed_overlay' | 'navigated_away' | 'no_interaction'
timestamp: this.getTimestamp()
});
}
}
// Initialize global instance
window.revelorDataLayer = new RevelorDataLayer();
// =============================================================================
// search-tracker.js
// =============================================================================
/**
* @fileoverview Search Tracker for Backend Analytics
* Tracks search events and clicks to backend API according to metrics-tracking.md
* @version 1.0.0
* @author Revelor by Webotvůrci
* @contact [email protected] | +420 728 089 029
*/
class SearchTracker {
constructor(config = {}) {
this.apiEndpoint = config.apiEndpoint || 'https://textilomanie.revelor.cz/api/search/track';
this.projectId = config.projectId || '312030';
this.sessionTimeout = config.sessionTimeout || 1800000; // 30 minut v ms
this.sessionKey = 'search_session_id';
this.lastSearchTimeKey = 'search_last_time';
// Generuj session ID pokud neexistuje nebo vypršela
this.getOrCreateSession();
}
/**
* Get or create session ID
*/
getOrCreateSession() {
const sessionId = sessionStorage.getItem(this.sessionKey);
const lastSearchTime = sessionStorage.getItem(this.lastSearchTimeKey);
const now = Date.now();
// Pokud session neexistuje nebo vypršela (30 min), vytvoř novou
if (!sessionId || !lastSearchTime || (now - parseInt(lastSearchTime)) > this.sessionTimeout) {
const newSessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem(this.sessionKey, newSessionId);
sessionStorage.setItem(this.lastSearchTimeKey, now.toString());
return newSessionId;
}
return sessionId;
}
/**
* Get current session ID
*/
getSessionId() {
return this.getOrCreateSession();
}
/**
* Update last search time
*/
updateLastSearchTime() {
sessionStorage.setItem(this.lastSearchTimeKey, Date.now().toString());
}
/**
* Track search event
* @param {Object} data - Search tracking data
* @param {string} data.query - Search query (min 3 chars)
* @param {number} [data.results_count] - Number of results
* @param {string} [data.search_type] - 'manual' | 'suggestion' | 'live_search'
*/
async trackSearch(data) {
const { query, results_count, search_type = 'live_search' } = data;
// Backend požaduje minimálně 3 znaky
if (!query || query.length < 3) {
console.log('🔇 Search tracking skipped - query too short:', query);
return;
}
const sessionId = this.getSessionId();
this.updateLastSearchTime();
const eventData = {
event_type: 'search',
query: query,
session_id: sessionId,
url: window.location.href,
referrer: document.referrer || '',
timestamp: new Date().toISOString(),
user_agent: navigator.userAgent,
search_type: search_type
};
if (results_count !== undefined) {
eventData.results_count = results_count;
}
await this.sendEvent(eventData);
}
/**
* Track click event
* @param {Object} data - Click tracking data
* @param {string} data.clicked_result - Product/article/category ID (required)
* @param {string} data.result_type - 'product' | 'article' | 'category'
* @param {string} data.result_title - Title of the result
* @param {string} data.result_url - URL of the result
* @param {number} data.result_position - Position in results (1-based)
* @param {string} data.query - Search query
*/
async trackClick(data) {
const { clicked_result, product_id, result_type, result_title, result_url, result_position, query } = data;
// Backend vyžaduje clicked_result nebo product_id
const resultId = clicked_result || product_id;
if (!resultId) {
console.error('❌ Click tracking error - missing clicked_result or product_id');
return;
}
const sessionId = this.getSessionId();
const eventData = {
event_type: 'click',
clicked_result: resultId,
result_type: result_type || 'product',
query: query || '',
session_id: sessionId,
url: window.location.href,
referrer: document.referrer || '',
timestamp: new Date().toISOString(),
user_agent: navigator.userAgent
};
if (result_title) {
eventData.result_title = result_title;
}
if (result_url) {
eventData.result_url = result_url;
}
if (result_position !== undefined) {
eventData.result_position = result_position;
}
// Alternativní název pro product_id (pro kompatibilitu)
if (product_id) {
eventData.product_id = product_id;
}
await this.sendEvent(eventData);
}
/**
* Send event to backend API
* @param {Object} eventData - Event data to send
*/
async sendEvent(eventData) {
try {
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant': this.projectId
},
body: JSON.stringify(eventData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error('❌ Search tracking error:', response.status, errorData);
return;
}
const responseData = await response.json().catch(() => ({}));
if (responseData.status === 'skipped') {
console.log('⏭️ Search tracking skipped:', responseData.message);
} else {
console.log('✅ Search tracking success:', responseData);
}
} catch (error) {
// Fail silently - tracking should not break the app
console.warn('⚠️ Search tracking failed:', error);
}
}
}
// Create global instance
window.searchTracker = new SearchTracker({
projectId: (typeof dataLayer !== 'undefined' &&
dataLayer[0] &&
dataLayer[0].shoptet &&
dataLayer[0].shoptet.projectId) || '312030'
});
// =============================================================================
// searching.js
// =============================================================================
/**
* @fileoverview Search Overlay Component
* Implements search functionality using Shoptet API
* @version 2.0.0
* @author Revelor by Webotvůrci
* @contact [email protected] | +420 728 089 029
*/
class SearchOverlay {
constructor() {
this.isOpen = false;
this.apiBaseUrl = 'https://textilomanie.revelor.cz/api/public';
this.projectId = this.getProjectId();
this.minSearchLength = 1;
this.previousActiveElement = null; // Store previous focus
this.focusableElements = []; // Focusable elements in overlay
this.currentFocusIndex = -1; // Current focus index for keyboard navigation
// Performance optimizations
this.abortController = null; // Request cancellation
this.cache = new Map(); // Simple cache
this.cacheTTL = 300000; // 5 minutes
// Search history
this.historyKey = 'revelor_search_history';
this.maxHistoryItems = 10;
this.init();
}
/**
* Initialize search overlay
*/
init() {
this.createOverlay();
this.bindEvents();
this.loadPlaceholders();
}
/**
* Get Project ID from dataLayer or use default
*/
getProjectId() {
if (
typeof dataLayer !== 'undefined' &&
dataLayer[0] &&
dataLayer[0].shoptet
) {
return dataLayer[0].shoptet.projectId;
}
return '312030'; // Default fallback
}
/**
* Create and inject search overlay HTML
*/
createOverlay() {
const overlayHTML = `
Vyhledávací dialog
Použijte toto pole k vyhledání produktů, kategorií, článků nebo značek. Stiskněte Escape pro zavření dialogu.
🔍
Zadejte hledaný výraz
Začněte psát a uvidíte návrhy
😔
Žádné výsledky
Zkuste jiný hledaný výraz
Kategorie
Produkty
Články
Značky
`;
// Insert before #content-wrapper or at the beginning of body
const contentWrapper = document.getElementById('content-wrapper');
if (contentWrapper) {
contentWrapper.insertAdjacentHTML('beforebegin', overlayHTML);
} else {
document.body.insertAdjacentHTML('afterbegin', overlayHTML);
}
}
/**
* Bind event listeners
*/
bindEvents() {
const formSearchForm = document.getElementById('formSearchForm');
const overlay = document.getElementById('search-overlay');
const input = document.getElementById('search-input');
const close = document.getElementById('search-close');
const submit = document.getElementById('search-submit');
// Open search from header form
if (formSearchForm) {
console.log(
'✅ SEARCH EVENTS - formSearchForm found, adding click listener'
);
formSearchForm.addEventListener('click', async e => {
console.log('🖱️ SEARCH EVENTS - formSearchForm clicked!');
e.preventDefault();
await this.open();
});
} else {
console.log('❌ SEARCH EVENTS - formSearchForm NOT FOUND!');
}
// Close search
close?.addEventListener('click', () => this.close());
overlay?.addEventListener('click', e => {
if (e.target === overlay) this.close();
});
// Submit search
submit?.addEventListener('click', () => {
const query = this.getCurrentQuery().trim();
if (query.length >= this.minSearchLength) {
this.redirectToSearchResults(query);
}
});
// Search input
input?.addEventListener('input', e => this.handleSearch(e.target.value));
input?.addEventListener('keydown', e => {
if (e.key === 'Escape') {
this.close();
} else if (e.key === 'Enter') {
e.preventDefault();
const query = this.getCurrentQuery().trim();
if (query.length >= this.minSearchLength) {
this.redirectToSearchResults(query);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
this.focusNextResult();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.focusPreviousResult();
}
});
// Keyboard navigation - global
document.addEventListener('keydown', e => {
if (e.key === '/' && !this.isOpen && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
e.preventDefault();
this.open();
}
// Handle Tab key for focus trap when overlay is open
if (this.isOpen && e.key === 'Tab') {
this.handleTabKey(e);
}
});
// Setup swipe to close (mobile)
this.setupSwipeToClose();
}
/**
* Setup swipe to close for mobile
*/
setupSwipeToClose() {
const overlay = document.getElementById('search-overlay');
if (!overlay) return;
let startY = 0;
let currentY = 0;
let isDragging = false;
overlay.addEventListener('touchstart', (e) => {
// Only if touch starts at the top of content
if (e.target.closest('.search-overlay__header')) {
startY = e.touches[0].clientY;
isDragging = true;
}
}, { passive: true });
overlay.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentY = e.touches[0].clientY;
const diff = currentY - startY;
// Only allow swipe down
if (diff > 0) {
const content = overlay.querySelector('.search-overlay__content');
if (content) {
content.style.transform = `translateX(-50%) translateY(${diff}px)`;
content.style.opacity = Math.max(0.5, 1 - diff / 400);
}
}
}, { passive: true });
overlay.addEventListener('touchend', () => {
if (!isDragging) return;
const diff = currentY - startY;
const content = overlay.querySelector('.search-overlay__content');
if (diff > 100) {
// Swipe down threshold - close overlay
this.close();
} else {
// Return to position
if (content) {
content.style.transform = 'translateX(-50%)';
content.style.opacity = '1';
}
}
isDragging = false;
startY = 0;
currentY = 0;
});
}
/**
* Handle Tab key for focus trap
*/
handleTabKey(e) {
const overlay = document.getElementById('search-overlay');
if (!overlay) return;
const focusableElements = overlay.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
// Tab
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
/**
* Focus next result in list
*/
focusNextResult() {
const results = this.getFocusableResults();
if (results.length === 0) return;
this.currentFocusIndex = (this.currentFocusIndex + 1) % results.length;
results[this.currentFocusIndex].focus();
}
/**
* Focus previous result in list
*/
focusPreviousResult() {
const results = this.getFocusableResults();
if (results.length === 0) return;
this.currentFocusIndex = this.currentFocusIndex <= 0 ? results.length - 1 : this.currentFocusIndex - 1;
results[this.currentFocusIndex].focus();
}
/**
* Get all focusable results
*/
getFocusableResults() {
const resultsContainer = document.getElementById('search-content-results');
if (!resultsContainer || resultsContainer.style.display === 'none') {
return [];
}
return Array.from(resultsContainer.querySelectorAll('a[href], button:not([disabled])'));
}
/**
* Load dynamic placeholders
*/
async loadPlaceholders() {
try {
const response = await fetch(
`${this.apiBaseUrl}/placeholders?projectId=${this.projectId}`,
{
headers: {
'X-Tenant': this.projectId,
},
}
);
const data = await response.json();
if (data.placeholders && data.placeholders.length > 0) {
const input = document.getElementById('search-input');
if (input) {
input.placeholder = data.placeholders[0];
}
}
} catch (error) {
console.warn('Failed to load placeholders:', error);
}
}
/**
* Load recommendations for empty state
*/
async loadRecommendations() {
console.log('🚀 Starting to load recommendations...');
console.log('API Base URL:', this.apiBaseUrl);
console.log('Project ID:', this.projectId);
try {
const url = `${this.apiBaseUrl}/recommendations?projectId=${this.projectId}`;
console.log('🔗 Fetching from URL:', url);
const response = await fetch(url, {
headers: {
'X-Tenant': this.projectId,
},
});
console.log('📡 Response status:', response.status);
console.log('📡 Response ok:', response.ok);
const data = await response.json();
console.log('🔍 SEARCH RECOMMENDATIONS API - Data loaded:', data);
return data;
} catch (error) {
console.error('❌ Failed to load recommendations:', error);
return null;
}
}
/**
* Open search overlay
*/
async open() {
console.log('🚀 SEARCH OVERLAY - Opening search overlay...');
// Store current focus to return later
this.previousActiveElement = document.activeElement;
// DataLayer: Search Started
if (window.revelorDataLayer) {
window.revelorDataLayer.searchStarted('overlay');
}
// Store start time for abandoned tracking
this.searchStartTime = Date.now();
this.lastSearchTerm = null;
this.lastResultsCount = 0;
this.isOpen = true;
const overlay = document.getElementById('search-overlay');
const input = document.getElementById('search-input');
if (overlay) {
overlay.classList.add('search-overlay--active');
document.body.classList.add('search-overlay-open');
console.log('🚀 SEARCH OVERLAY - Overlay activated');
} else {
console.log('❌ SEARCH OVERLAY - Overlay element not found!');
}
// Update aria-expanded on input
if (input) {
input.setAttribute('aria-expanded', 'true');
}
// Focus input after animation
setTimeout(async () => {
input?.focus();
console.log('🚀 SEARCH OVERLAY - Input focused');
// Show empty state with recommendations
await this.showEmptyState();
}, 100);
}
/**
* Close search overlay
*/
close() {
// DataLayer: Search Abandoned (if there was a search term but no click)
if (window.revelorDataLayer && this.lastSearchTerm && this.searchStartTime) {
const timeSpent = ((Date.now() - this.searchStartTime) / 1000).toFixed(1);
window.revelorDataLayer.searchAbandoned(
this.lastSearchTerm,
this.lastResultsCount,
timeSpent,
'closed_overlay'
);
}
this.isOpen = false;
const overlay = document.getElementById('search-overlay');
const input = document.getElementById('search-input');
if (overlay) {
overlay.classList.remove('search-overlay--active');
document.body.classList.remove('search-overlay-open');
}
// Update aria-expanded on input
if (input) {
input.setAttribute('aria-expanded', 'false');
}
// Return focus to previous element
setTimeout(() => {
if (this.previousActiveElement && this.previousActiveElement.focus) {
this.previousActiveElement.focus();
}
}, 100);
this.clearSearch();
}
/**
* Clear search input and results
*/
async clearSearch() {
const input = document.getElementById('search-input');
if (input) {
input.value = '';
input.focus();
}
await this.showEmptyState();
}
/**
* Get current search query
*/
getCurrentQuery() {
const input = document.getElementById('search-input');
return input ? input.value : '';
}
/**
* Format price with dot as thousands separator and remove unnecessary decimals
*/
formatPrice(price) {
if (!price) return '';
// Convert to number
const numPrice = parseFloat(price);
// Format as integer (bez desetinných míst) s tečkou jako oddělovač tisíců
return Math.floor(numPrice).toLocaleString('cs-CZ', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}
/**
* Redirect to search results page
*/
redirectToSearchResults(query) {
const url = `/vyhledavani/?string=${encodeURIComponent(query)}`;
console.log('Redirecting to search results:', url);
// Create temporary link and click it
const tempLink = document.createElement('a');
tempLink.href = url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
}
/**
* Add product to cart (Shoptet API) - for Search Overlay
* @param {Object} product - Product data
* @param {HTMLElement} button - Button element
*/
async addToCart(product, button) {
const originalHTML = button.innerHTML;
// Show loading state
button.disabled = true;
button.classList.add('loading');
button.style.opacity = '0.5';
try {
// Check if Shoptet API is available
if (typeof shoptet === 'undefined' || !shoptet.cartShared) {
throw new Error('Shoptet API není k dispozici');
}
// Determine which method to use (podle Shoptet dokumentace)
let addParams = {};
if (product.priceId) {
// Nejlepší varianta: priceId
addParams = { priceId: product.priceId };
} else if (product.kod || product.code) {
// Fallback: product code
addParams = { productCode: product.kod || product.code };
} else if (product._id || product.id) {
// Poslední varianta: product ID
addParams = { productId: product._id || product.id };
} else {
throw new Error('Nelze identifikovat produkt');
}
// Add to cart (silent mode = true to prevent Shoptet's modal)
await shoptet.cartShared.addToCart(addParams, true);
// Success feedback
button.classList.remove('loading');
button.classList.add('success');
button.style.opacity = '1';
// TODO: Add toast notification if available
// Tracking
if (window.revelorDataLayer) {
window.dataLayer.push({
event: 'revelor_quick_add_to_cart',
product_id: product._id || product.id,
product_name: product.title,
price: product.price_s_dph,
source: 'search_overlay'
});
}
// Reset button after 2s
setTimeout(() => {
button.disabled = false;
button.classList.remove('success');
button.style.opacity = '';
}, 2000);
} catch (error) {
console.error('Add to Cart Error:', error);
button.innerHTML = originalHTML;
button.disabled = false;
button.classList.remove('loading');
button.style.opacity = '';
// TODO: Add error notification
}
}
/**
* Handle search input
*/
async handleSearch(query) {
if (query.length < this.minSearchLength) {
await this.showEmptyState();
return;
}
// Only show loading if no results are currently displayed
const resultsContainer = document.getElementById('search-content-results');
const isShowingResults =
resultsContainer && resultsContainer.style.display !== 'none';
if (!isShowingResults) {
this.showLoadingState();
}
// Perform search immediately
this.performSearch(query);
}
/**
* Perform search using API
*/
async performSearch(query) {
const searchUrl = `${this.apiBaseUrl}/suggestions?q=${encodeURIComponent(query)}&projectId=${this.projectId}&products_limit=6&categories_limit=6&articles_limit=6&brands_limit=0`;
// DataLayer: Search Query Submitted
if (window.revelorDataLayer) {
window.revelorDataLayer.searchQuerySubmitted(query, 'overlay');
}
// Store for refinement tracking
this.lastSearchTerm = query;
// Check cache first
const cacheKey = `${query}_${this.projectId}_suggestions`;
const cached = this.getFromCache(cacheKey);
if (cached) {
console.log('📦 Using cached results for:', query);
this.displayResults(cached, 0);
return;
}
// Cancel previous request
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
const startTime = Date.now();
try {
const response = await this.fetchWithRetry(searchUrl, {
signal: this.abortController.signal,
headers: {
'X-Tenant': this.projectId,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const responseTime = Date.now() - startTime;
// Save to cache
this.saveToCache(cacheKey, data);
// Add to search history if has results
const hasResults = data.products?.length > 0 || data.categories?.length > 0 || data.articles?.length > 0;
if (hasResults) {
this.addToSearchHistory(query);
}
this.displayResults(data, responseTime);
} catch (error) {
if (error.name === 'AbortError') {
console.log('🚫 Request cancelled for:', query);
return;
}
console.error('Search API Error:', error);
// Try to use cached results as fallback
const fallbackCached = this.getFromCache(cacheKey, true);
if (fallbackCached) {
console.log('📦 Using stale cache as fallback');
this.displayResults(fallbackCached, 0);
this.announceToScreenReader('Zobrazuji uložené výsledky (offline)');
return;
}
this.showErrorState();
}
}
/**
* Save to cache
*/
saveToCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
/**
* Get from cache
*/
getFromCache(key, ignoreExpiry = false) {
const cached = this.cache.get(key);
if (!cached) return null;
// Check TTL (unless ignoreExpiry for fallback)
if (!ignoreExpiry && Date.now() - cached.timestamp > this.cacheTTL) {
this.cache.delete(key);
return null;
}
return cached.data;
}
/**
* Announce message to screen readers
*/
announceToScreenReader(message) {
const liveRegion = document.getElementById('search-live-region');
if (liveRegion) {
liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
}
}
/**
* Display search results
*/
displayResults(data, responseTime = 0) {
this.hideAllStates();
const hasResults =
data.products?.length > 0 ||
data.categories?.length > 0 ||
data.articles?.length > 0;
if (!hasResults) {
this.showNoResultsState();
this.announceToScreenReader('Žádné výsledky nebyly nalezeny');
// DataLayer: Search No Results
if (window.revelorDataLayer && this.lastSearchTerm) {
window.revelorDataLayer.searchNoResults(this.lastSearchTerm);
}
this.lastResultsCount = 0;
return;
}
this.showResultsState();
// Count results
const totalResults =
(data.products?.length || 0) +
(data.categories?.length || 0) +
(data.articles?.length || 0);
// Store for abandoned tracking
this.lastResultsCount = totalResults;
// Announce results
const resultTypes = [];
if (data.products?.length) resultTypes.push(`${data.products.length} ${data.products.length === 1 ? 'produkt' : 'produkty'}`);
if (data.categories?.length) resultTypes.push(`${data.categories.length} ${data.categories.length === 1 ? 'kategorie' : 'kategorie'}`);
if (data.articles?.length) resultTypes.push(`${data.articles.length} ${data.articles.length === 1 ? 'článek' : 'články'}`);
this.announceToScreenReader(`Nalezeno ${totalResults} výsledků: ${resultTypes.join(', ')}`);
// DataLayer: Search Results Displayed
if (window.revelorDataLayer && this.lastSearchTerm) {
const resultsData = {
products: data.products || [],
categories: data.categories || [],
articles: data.articles || [],
total_products: totalResults
};
window.revelorDataLayer.searchResultsDisplayed(
this.lastSearchTerm,
resultsData,
responseTime,
1
);
}
// Backend API: Track search event
if (window.searchTracker && this.lastSearchTerm && this.lastSearchTerm.length >= 3) {
window.searchTracker.trackSearch({
query: this.lastSearchTerm,
results_count: totalResults,
search_type: 'live_search'
});
}
// Display products
if (data.products?.length > 0) {
this.displayProducts(data.products);
}
// Display categories
if (data.categories?.length > 0) {
this.displayCategories(data.categories);
}
// Display articles
if (data.articles?.length > 0) {
this.displayArticles(data.articles);
}
// Reset focus index
this.currentFocusIndex = -1;
}
/**
* Display products
*/
displayProducts(products) {
const container = document.getElementById('search-products');
const list = document.getElementById('search-products-list');
const showAll = document.getElementById('search-products-show-all');
if (!container || !list) return;
container.style.display = 'block';
list.innerHTML = '';
products.forEach((product, index) => {
const item = document.createElement('div');
item.className = 'search-overlay__product';
item.setAttribute('role', 'listitem');
// Microdata attributes for AI agents
item.setAttribute('itemscope', '');
item.setAttribute('itemtype', 'https://schema.org/Product');
const priceText = product.price_s_dph ? `${this.formatPrice(product.price_s_dph)} Kč` : 'Cena na dotaz';
const availabilityText = product.skladem ? 'Skladem' : 'Není skladem';
const availabilitySchema = product.skladem ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock';
// Create JSON-LD structured data
const jsonLd = {
"@context": "https://schema.org/",
"@type": "Product",
"@id": product._id,
"name": product.title,
"description": product.description || product.title,
"image": product.img,
"url": product.url,
"sku": product.kod,
"offers": {
"@type": "Offer",
"price": product.price_s_dph || 0,
"priceCurrency": "CZK",
"availability": availabilitySchema,
"url": product.url
}
};
item.innerHTML = `
${product.img ? `

` : ''}
${this.highlightSearchTerm(product.title, this.lastSearchTerm)}
${product.price_s_dph ? this.formatPrice(product.price_s_dph) + ' Kč' : 'Cena na dotaz'}
${product.skladem && !product.has_variants ? `
` : ''}
`;
// Add click event for tracking
const link = item.querySelector('.search-overlay__product-link');
if (link) {
link.addEventListener('click', () => {
// DataLayer: Search Result Click
if (window.revelorDataLayer && this.lastSearchTerm) {
window.revelorDataLayer.searchResultClick(
this.lastSearchTerm,
'product',
product,
index + 1,
this.lastResultsCount
);
}
// Backend API: Track click event
if (window.searchTracker && this.lastSearchTerm) {
window.searchTracker.trackClick({
clicked_result: product._id || product.id,
product_id: product._id || product.id,
result_type: 'product',
result_title: product.title,
result_url: product.url,
result_position: index + 1,
query: this.lastSearchTerm
});
}
});
}
// Add click event for "Add to Cart" button
const addBtn = item.querySelector('.search-overlay__product-add-btn');
if (addBtn) {
addBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.addToCart(product, addBtn);
});
}
list.appendChild(item);
});
if (showAll) {
showAll.style.display = 'block';
// Set up click handler for "Show all" link
const showAllLink = showAll.querySelector(
'.search-overlay__show-all-link'
);
if (showAllLink) {
showAllLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
console.log('Redirecting to:', url);
// Create temporary link and click it
const tempLink = document.createElement('a');
tempLink.href = url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
return false;
});
}
}
}
/**
* Display categories
*/
displayCategories(categories) {
const container = document.getElementById('search-categories');
const list = document.getElementById('search-categories-list');
const showAll = document.getElementById('search-categories-show-all');
if (!container || !list) return;
container.style.display = 'block';
list.innerHTML = '';
categories.forEach((category, index) => {
const item = document.createElement('div');
item.className = 'search-overlay__category';
item.setAttribute('role', 'listitem');
item.innerHTML = `
${category.title}
`;
// Add click tracking for category
const link = item.querySelector('.search-overlay__category-link');
if (link && window.searchTracker && this.lastSearchTerm) {
link.addEventListener('click', () => {
window.searchTracker.trackClick({
clicked_result: category._id || category.id || category.url,
result_type: 'category',
result_title: category.title,
result_url: category.url,
result_position: index + 1,
query: this.lastSearchTerm
});
});
}
list.appendChild(item);
});
if (showAll) {
showAll.style.display = 'block';
// Set up click handler for "Show all" link
const showAllLink = showAll.querySelector(
'.search-overlay__show-all-link'
);
if (showAllLink) {
showAllLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
console.log('Redirecting to:', url);
// Create temporary link and click it
const tempLink = document.createElement('a');
tempLink.href = url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
return false;
});
}
}
}
/**
* Display articles
*/
displayArticles(articles) {
const container = document.getElementById('search-articles');
const list = document.getElementById('search-articles-list');
const showAll = document.getElementById('search-articles-show-all');
if (!container || !list) return;
container.style.display = 'block';
list.innerHTML = '';
articles.forEach((article, index) => {
const item = document.createElement('div');
item.className = 'search-overlay__article';
item.setAttribute('role', 'listitem');
const description = article.description || '';
const ariaLabel = description ? `Článek ${article.title}: ${description}` : `Článek ${article.title}`;
item.innerHTML = `
${article.img ? `

` : ''}
${article.title}
${description ? `
${description}
` : ''}
`;
// Add click tracking for article
const link = item.querySelector('.search-overlay__article-link');
if (link && window.searchTracker && this.lastSearchTerm) {
link.addEventListener('click', () => {
window.searchTracker.trackClick({
clicked_result: article._id || article.id || article.url,
result_type: 'article',
result_title: article.title,
result_url: article.url,
result_position: index + 1,
query: this.lastSearchTerm
});
});
}
list.appendChild(item);
});
if (showAll) {
showAll.style.display = 'block';
// Set up click handler for "Show all" link
const showAllLink = showAll.querySelector(
'.search-overlay__show-all-link'
);
if (showAllLink) {
showAllLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
console.log('Redirecting to:', url);
// Create temporary link and click it
const tempLink = document.createElement('a');
tempLink.href = url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
return false;
});
}
}
}
/**
* Display brands
*/
displayBrands(brands) {
const container = document.getElementById('search-brands');
const list = document.getElementById('search-brands-list');
const showAll = document.getElementById('search-brands-show-all');
if (!container || !list) return;
container.style.display = 'block';
list.innerHTML = '';
brands.forEach((brand, index) => {
console.log('Brand data:', brand); // Debug log
const item = document.createElement('div');
item.className = 'search-overlay__brand';
if (brand.url) {
// Brand má URL - vytvoř proklik
item.innerHTML = `
${brand.title}
${brand.description || ''}
`;
// Add click tracking for brand (treat as category for now)
const link = item.querySelector('.search-overlay__brand-link');
if (link && window.searchTracker && this.lastSearchTerm) {
link.addEventListener('click', () => {
window.searchTracker.trackClick({
clicked_result: brand._id || brand.id || brand.url,
result_type: 'category', // Brands are tracked as categories
result_title: brand.title,
result_url: brand.url,
result_position: index + 1,
query: this.lastSearchTerm
});
});
}
} else {
// Brand nemá URL - zobraz bez prokliku
item.innerHTML = `
${brand.title}
${brand.description || ''}
`;
}
list.appendChild(item);
});
if (showAll) {
showAll.style.display = 'block';
// Set up click handler for "Show all" link
const showAllLink = showAll.querySelector(
'.search-overlay__show-all-link'
);
if (showAllLink) {
showAllLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const url = `/vyhledavani/?string=${encodeURIComponent(this.getCurrentQuery())}`;
console.log('Redirecting to:', url);
// Create temporary link and click it
const tempLink = document.createElement('a');
tempLink.href = url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
return false;
});
}
}
}
/**
* Show loading state
*/
showLoadingState() {
this.hideAllStates();
const loading = document.getElementById('search-loading');
if (loading) loading.style.display = 'block';
}
/**
* Show empty state
*/
async showEmptyState() {
console.log('📋 SEARCH EMPTY STATE - Starting showEmptyState()');
this.hideAllStates();
const empty = document.getElementById('search-empty');
if (empty) {
empty.style.display = 'block';
console.log('📋 SEARCH EMPTY STATE - Empty element displayed');
// Show search history if available
this.displaySearchHistory();
} else {
console.log('❌ SEARCH EMPTY STATE - Empty element not found!');
}
// Load recommendations for empty state
console.log('📋 SEARCH EMPTY STATE - About to load recommendations...');
const recommendations